今天我們要重新檢視 React 開發中最基礎、卻也最容易踩坑的 Hook:useState。我們將從底層的單向鏈表結構,聊到為什麼 React 堅持 Immutability(不可變性)。
在前幾天,我們用閉包簡單模擬了 useState 的運作。但在真實的 React Fiber 架構中,useState 的 State 到底是存放在哪裡的?
答案是:存放在組件對應的 Fiber 節點上的 memoizedState 鏈表中。
[Fiber Node]
└── memoizedState ──► [Hook 1: useState]
└── next ──► [Hook 2: useEffect]
└── next ──► [Hook 3: useState]
為什麼 Hook 不能寫在 if 或 for 迴圈內?
每次呼叫 useState 時,React 會順著鏈表上的 next 指標依序取出對應的 Hook 節點。如果將 Hook 放在條件判斷式中,導致前後兩次渲染的 Hook 呼叫順序不一致,指標就會錯位,取出錯誤的 State 數據!
在 React 中更新狀態時,我們絕不能直接修改原本的物件或陣列,而必須傳入一個全新的物件/陣列參考 (Reference)。
效能優化:Object.is 同值比對
當你呼叫 setState 時,React 會呼叫內部的 Object.is(oldState, newState) 來比較前後狀態。
若直接修改 (Mutation):oldState 與 newState 指向同一塊記憶體位址,Object.is 回傳 true -> React 認定狀態未改變,直接跳過 (Bailout) Re-render!
傳入新物件 (Immutable):newState 擁有全新的記憶體位址,Object.is 回傳 false -> 成功觸發 Re-render。
const [user, setUser] = useState({ name: 'Alice', score: 100 });
// 錯誤:直接修改原物件,記憶體參考未變,畫面不會更新!
user.score = 200;
setUser(user);
// 正確:透過展開運算子建立全新的物件參考
setUser({
...user,
score: 200
});
const [count, setCount] = useState(0);
const handleClick = () => {
// 三次呼叫都拿到當前閉包快照中的 count (0),最終只加 1
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
// 傳入函數,React 會將更新放入 Queue 依序執行,最終加 3
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
};
// 每次組件 Re-render 都會重複執行昂貴的 parse 計算
const [data, setData] = useState(parseExpensiveData());
// 傳入初始化函數 (Lazy Initial State),該計算只會在 Mount 時執行一次!
const [data, setData] = useState(() => parseExpensiveData());